package org.edith.shiro.component;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.UnauthenticatedException;
import org.edith.shiro.constant.RedisConstant;
import org.edith.shiro.context.WebContext;
import org.edith.shiro.dataobject.SysMenuDO;
import org.edith.shiro.dataobject.SysRoleDO;
import org.edith.shiro.dataobject.SysUserDO;
import org.edith.shiro.enums.UserStatusEnum;
import org.edith.shiro.need.implement.UserStatusHandler;
import org.edith.shiro.service.SysMenuService;
import org.edith.shiro.service.SysRoleService;
import org.edith.shiro.service.SysUserService;
import org.edith.shiro.util.SHA256Util;
import org.edith.shiro.util.ShiroUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 *  Shiro权限匹配和账号密码匹配
 *
 */
@Component("edithShiroRealm")
public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysMenuService sysMenuService;
    @Autowired
    private RedisSessionDAO redisSessionDAO;
    @Autowired
    private RedisTemplate<String,String>  redisTemplate;
    @Autowired(required = false)
    private UserStatusHandler userStatusHandler;

    /**
     * 授权权限
     * 用户进行权限验证时候Shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        // 如果能执行到这里，token一定有值，且用户一定已登录
        String token = WebContext.getThreadLocal().get().get( "token" );

        String ipAddress = WebContext.getThreadLocal().get().get( "ipAddress" );

        String[] arr = token.split( "_" );
        // 获取最后一位加密值
        String code = arr[arr.length-1];
        String temp = DigestUtils.md5DigestAsHex( SHA256Util.sha256( ipAddress, RedisConstant.REDIS_PREFIX_LOGIN ).getBytes() );
        /*
         * todo
         * 这么做的目的是为了防止盗用token,将token与客户端的IP地址进行绑定，
         * 虽然不能完全杜绝盗用token，至少也能将用户的token做一层安全保障
         * 注意：弱网环境和频繁变换IP环境 慎用，可将此代码注释调即可
         */
        if(!code.equals( temp )){
            throw new UnauthenticatedException();
        }


        //获取用户ID
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        SysUserDO sysUserDO = (SysUserDO) principalCollection.getPrimaryPrincipal();
        Long userId = sysUserDO.getUserId();
        //这里可以进行授权和处理
        Set<String> rolesSet = new HashSet<>();
        Set<String> permsSet = new HashSet<>();
        //查询角色和权限(这里根据业务自行查询)
        List<SysRoleDO> sysRoleDOList = sysRoleService.selectSysRoleByUserId(userId);
        for (SysRoleDO sysRoleDO : sysRoleDOList) {
            rolesSet.add(sysRoleDO.getRoleName());
            List<SysMenuDO> sysMenuDOList = sysMenuService.selectSysMenuByRoleId(sysRoleDO.getRoleId());
            for (SysMenuDO sysMenuDO : sysMenuDOList) {
                permsSet.add(sysMenuDO.getPerms());
            }
        }
        //将查到的权限和角色分别传入authorizationInfo中
        authorizationInfo.setStringPermissions(permsSet);
        authorizationInfo.setRoles(rolesSet);
        redisTemplate.expire( token,30, TimeUnit.MINUTES );
        SecurityUtils.getSubject().getSession().setTimeout(30*60*60);
        return authorizationInfo;
    }

    /**
     * 身份认证
     *
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获取用户的输入的账号.
        String username = (String) authenticationToken.getPrincipal();
        //通过username从数据库中查找 User对象，如果找到进行验证
        //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        SysUserDO user = sysUserService.selectUserByName(username);
        //判断账号是否存在
        if (user == null) {
            throw new AuthenticationException();
        }
        if(userStatusHandler == null){
            //判断账号是否被冻结
            if (user.getStatus()==null|| UserStatusEnum.PROHIBIT.name().equals(user.getStatus())){
                throw new LockedAccountException();
            }
        }else{
            // 如果存在用户的自定义处理流程
            userStatusHandler.statusHandler( user );
        }
        //进行验证
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user,                                  //用户名
                user.getPassword(),                    //密码
                ByteSource.Util.bytes(user.getSalt()), //设置盐值
                getName()
        );
        //验证成功开始踢人(清除缓存和Session)
        ShiroUtils.deleteCache(username,true);
        return authenticationInfo;
    }
}
